################################################################################
# a general example
################################################################################


include Dbside

DB = Postgres::Database.new(
  :database => 'reality',
  :user => 'postgres',
  :password => '12345'
)

class Nodes < DB::Table
  set_table_name :my_nodes
 
  # associations
  one_to_one :parent_node, :class => Nodes, :match => {:id => :parent_id}
  one_to_many :states, :match {:node_id => :id}, :cascade => :all # :class => States is implied
  one_to_many :children, :class => Nodes, :match {:parent_id => :id},
    :cascade => :delete
  one_to_one :producer, :class => Channels, :match {:id => :producer_id}

  # method hooks
  before_delete :remove_from_producer

  def remove_from_producer
  ...
  end

  # another way to do the hook is this:
  before_delete do
    ...
  end

  # custom methods
  def set_state(opts)
    ...
    update_all_subscribers
  end

  def write(value, datatype = nil)
    if producer
      producer.write_request(self, value, datatype)
    else
      set_state(:quality => :good, :value => value, :datatype => datatype)
    end
  end
end

class States < DB::Table
  Qualities = {0 => :unknown, 1 => :good, 2 => :bad, 3 => :invalid}.freeze
  Datatypes = {0 => :text, 1 => :integer, 2 => :float, 3 => :boolean}

  translate :quality => Qualities
  translate :datatype => Datatypes
 
end

class Nodes < DB::Table
  translate :state_datatype => States::Datatypes

  translate :state_stamp, :fetch => proc {|s| Time.at(s)}, :update => proc {|s| s.to_f}

  # or maybe
  translate :state_stamp => Time
end

################################################################################
# database connection
################################################################################

DB = ServerSide::Database::Postgres.new(
  {:database => 'reality', :user => 'postgres', :password => '12345'})

class ServerSide::Database::Base
  def [](tblname)
    query(:source => tblname)
  end
end

################################################################################
# SQL DSL 
################################################################################

# Assign a table dataset to a constant
Posts = DB[:posts]

# A blank query
d = Dataset.new

Dataset.from(:posts)
#=> SELECT * FROM posts

Dataset.select(:id, :title).from(:posts)
#=> SELECT id, title FROM posts

Dataset.from(:posts).filter(:subject => 'music')
#=> SELECT * FROM posts WHERE subject = 'music'

Dataset.from(:posts).order(:stamp.DESC)
#=> SELECT * FROM posts ORDER BY stamp DESC

Dataset.from(:posts).limit(30)
#=> SELECT * FROM posts LIMIT 30

# pagination
Dataset.paginate(20, 1)
#=> SELECT * FROM posts LIMIT 0, 20
Dataset.paginate(20, 6)
#=> SELECT * FROM posts LIMIT 100, 20

################################################################################
# filter_by_XXX, order_by_XXX - auto-generated custom methods
################################################################################

DB.source(:posts).filter(:subject => /music/)
DB[:posts].filter(:subject => /music/)
DB[:posts].filter_by_subject(/music/) # automatically created filter_by_XXX method

Posts.filter_by_subject(/music/).order_by_stamp_DESC  # automatically created order_by_XXX method

# select specific columns
Posts.select_id_and_title.order_by_title # automatically created select_XXX method.

################################################################################
# filter expressions
################################################################################

filter(:stamp => t1..t2)
filter(:id => [1, 76, 23, 45])
filter(:title => /prototype/)
filter(:title.NOT.LIKE => 'john%')
filter(:title.START_WITH => 'john')
filter(:title.END_WITH =>'john')
filter(:id.NOT => [6, 5, 32, 2])

################################################################################
# joins
################################################################################

# join tables
Posts.join(:authors, :author_id => :id)

# default join is LEFT OUTER JOIN, but there are methods for other joins
Posts.inner_join(...)
Posts.left_outer_join(...) # join is an alias for this method
Posts.right_outer_join(...)
Posts.full_outer_join(...)

################################################################################
# selecting single records
################################################################################

n = Nodes.find(:path => 'users'/user)
n = Nodes.by_path('users'/user) # automatically created by_XXX method

class Nodes < Table
  default_find_by :path # redefines the [] method
end

n = Nodes['users'/user]

Nodes.first(:path => 'users'/user)
Nodes.last(:path => 'users'/user)
Nodes.first_by_path('users'/user) # etc.

# find_by_XXX and by_XXX methods raise an exception if record not found
# first_XXX and last_XXX methods return nil if record not found

################################################################################
# accessing record values
################################################################################

Nodes.each {|n| puts n[:title]} # each record is fetched and sent to the block
# the n in the block is a reference to the query itself

Nodes.all #=> returns an array with all records as hashes

Nodes.first #=> fetches the first record returns a reference to self with the record values loaded, or nil if no record
Nodes.last #=> fetches the last record and returns a reference to self with record values loaded, or nil of no record

Nodes['tprs/Bazra'] #=> fetches the first record that has the matching path

################################################################################
# summarizing
################################################################################

Nodes.count #=> returns the number of 
n = Nodes.group_by_quality.summarize(:quality, :id.COUNT, :value.AVG) #=>
  # returns an array of hashes:
  # [{:quality => 0, :count => 34, 'value average' => 43.12}, ...]
Nodes.group_by_quality.count #=> returns an hash:
# {0 => 23, 1 => 65, 3 => 44, 2 => 123}
Nodes.AVG(:value) #=> returns the average value
Nodes.group_by_quality.AVG(:value) #=> returns a hash:
#  {0 => 43.12, 1 => 54, 2 => 5.213}


################################################################################
# Creating records
################################################################################

n = Nodes.create(:name => 'sharon', :age => 32) #=> Dataset.new(:id => 1342876)
n[:id] #=> 1342876
n[:name] #=> 'sharon'

# Dataset is a descendant of Hash.
# When a record is fetched, the values are put into the Dataset.

class Dataset
  def create(values)
    ... # perform query
    where(:id => last_created_id).fetch
  end
  
  def fetch # fetches the next record in the query result
    ... # copy values to self
    self
  end
  
  def []= (field, value)
    update(field => value)
  end
end

################################################################################
# Updating records
################################################################################

Nodes.by_path('/tprs').update(:active => false)
Nodes.filter_by_path(/^\/tprs/).update(:active => false)
Nodes.filter_by_path(/^\/tprs/)[:active] = false

Nodes.filter_by_path(/^\/cex/).each {|n| n[:active] = !n[:active]}

################################################################################
# Deleting records
################################################################################

Nodes.find_by_path('/tprs').delete

################################################################################
# Individual records are really datasets
################################################################################

node = Nodes['/tprs']
node.is_a?(Dataset) #=> true

node[:active] = false
node.update(:active => false)

node[:active] #=> true/false

node.delete

################################################################################
# Defining a table class
################################################################################

class Nodes < Dbside::Table
end

################################################################################
# Relations
################################################################################

class Posts < Dbside::Table
  has_one :category, :table => :categories, :match => {:category_id => :id}
  
end

################################################################################
# Custom methods
################################################################################

Nodes.by_path('/tprs').children

class Nodes
  def children  
    Nodes.filter_by_parent_id(@record[:id])
  end
end

################################################################################
# relationships
################################################################################

class Nodes
  one_to_many :states, :match => {:id => :node_id}
  #synonimous to...
  def states
    States.filter(:node_id => @record[:id])
  end
  
  one_to_one :producer, :table => :channels, :match => {:producer_id => :id}
  #synonimous to...
  def producer
    Channels.first(:id => @record[:producer_id])
  end
end

# Let's take the code from
# http://blog.hasmanythrough.com/articles/2006/09/09/finding-unassociated-objects
# and see how it's done using our DSL

class Managers < Table
  # We use this to change the basic query for the table.
  # This is how we can create subsets.
  basic_dataset source(:personnel).filter(:type => 'manager')

  one_to_many :employees, :table => :personnel, :match {:id => :manager_id}
  # equivalent to
  def employees
    db[:personnel].filter(:manager_id => @record[:id])
  end
  
  def not_employees
    db[:personnel].filter(:manager_id.NOT => @record[:id])
  end
  
  def self.without_employees
    @@without_employees ||=
    left_outer_join('employees e', :id => 'e.manager_id').filter('e.id IS NULL')
  end
end

class Employees < Table
  basic_dataset source(:personnel).filter(:type => 'employee')
  
  one_to_one :manager, :table => :personnel, :match {:manager_id => :id}
  # equivalent to
  def manager
    db[:personnel].filter(:id => @record[:manager_id])
  end
  
  query :managed, filter(:manager_id.IS_NOT_NULL)
  query :orphans, filter(:manager_id.IS_NULL)
end





################################################################################
# Some code
################################################################################
class Dataset < Hash
  def self.query(name, dataset = nil, &block)
    puts "query"
    @queries ||= {}
    meta_def(name) {|*args| dataset || block.call(args)}
  end
  
  def self.method_missing(name, *args)
    case name.to_s
    when /^filter_by_(.+)$/:
      expressions = $1.split('_and_')
      query(name) do |a|
        filter(expressions.inject({}) {|m, i| m[i]=a.shift; m})
      end
    when /^(?:first_by_|by_)(.+)$/:
      expressions = $1.split('_and_')
      meta_def(name) {|*a| filter(expressions.inject({}) {|m, i| m[i]=a.shift; m}).first}
    when /^last_by_(.+)$/:
      expressions = $1.split('_and_')
      meta_def(name) {|*a| filter(expressions.inject({}) {|m, i| m[i]=a.shift; m}).last}
    when /^order_by_(.+)$/:
      expressions = $1.split('_and_')
      meta_def(name) {order(expressions)}
    else # create stub class.methods that create new queries and then call the corresponding
      # instance method.
      if instance_methods.include?(name.to_s)
        meta_def(name) do |*a|
          new.set_source(name.downcase).send(name, *a)
          # Create a new dataset, set its source to the table (class name in 
          # lower case) and invoke the modifier
        end
      end
    end
    send(name, *args) if respond_to?(name)
  end
  
  def self.[]=(name, value)
    make_query(name) {value}
  end
  
  def self.[](name)
    send(name)
  end
  
  def filter(*args)
    puts "filter(#{args.inspect})"
    dup.add_filter(*args) # create a copy of self and add the filter
  end
  
  def order(*args)
    puts "order(#{args.inspect})"
    dup.set_order(*args) # create a copy of self and set the order
  end
end

class Symbol
  def DESC
    "#{to_s} DESC"
  end
  
  def SUM
    "SUM(#{self})"
  end
end

class Table < Dataset
  def self.inherited(c)
    name = c.name.split('::').last
    c = eval(name)
    c.module_def(:table_dataset) {Dataset.new.from(name)}
  end
end

class Nodes < Table
  default_find_by :path
  query :music_posts, filter_by_subject('music').order(:stamp.DESC)
  query :posts_by_subject {|s| filter_by_subject(s).order_by_stamp_DESC}
  
  def print_music_posts
    music_posts.each {|p| puts p[:title]}
  end
end

